在前幾天,我們完成了單元測試、pprof 優化,以及 SearchService 介面層。今天的目標是:
/search 呼叫到 service,再到假 ES」的 整合測試。Elasticsearch 很重,不可能每次 CI/CD 都啟一個 cluster。
這時候,我們會用 fake service 來模擬 ES 行為:
前一天我們有定義:
// search.go
type SearchService interface {
    Search(ctx context.Context, query string) ([]SearchResult, error)
}
type SearchResult struct {
    ID    string
    Title string
}
今天我們新增一個 fake 版本:
// fake_es.go
package search
import "context"
type FakeSearchService struct{}
func (f *FakeSearchService) Search(ctx context.Context, query string) ([]SearchResult, error) {
    if query == "golang" {
        return []SearchResult{
            {ID: "1", Title: "Golang Official"},
            {ID: "2", Title: "Go by Example"},
        }, nil
    }
    return []SearchResult{}, nil
}
我們的 /search handler 透過 DI(依賴注入)傳入 service:
// handler.go
type SearchHandler struct {
    Service SearchService
}
func (h *SearchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    q := r.URL.Query().Get("q")
    results, err := h.Service.Search(ctx, q)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(results)
}
接著,用 httptest 模擬 HTTP 請求,寫第一個整合測試:
// handler_test.go
func TestSearchHandler_E2E(t *testing.T) {
    fake := &search.FakeSearchService{}
    h := &SearchHandler{Service: fake}
    req := httptest.NewRequest("GET", "/search?q=golang", nil)
    w := httptest.NewRecorder()
    h.ServeHTTP(w, req)
    resp := w.Result()
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        t.Fatalf("expected 200, got %d", resp.StatusCode)
    }
    var got []search.SearchResult
    if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
        t.Fatalf("decode error: %v", err)
    }
    if len(got) != 2 {
        t.Errorf("expected 2 results, got %d", len(got))
    }
}
這裡我們完全沒有啟動 ES,只是透過假 service,驗證 API → Service → Response 這條路徑是通的。
今天我們完成了 E2E 測試雛型:
httptest 驗證整條 API 流程。明天,我們就要真的打通 Elasticsearch,寫一個最小 smoke test,讓 /search 能對接本地 ES。